# Chapter 1 The CLR’s Execution Model

# Compiling Source Code into Managed Modules

The core features of the CLR (such as memory management, assembly loading, security, exception handling, and thread synchronization) are available to any and all programming languages that target it—period.

The common language runtime (CLR) is just what its name says it is: a runtime that is usable by
different and varied programming languages. The core features of the CLR (such as memory management, assembly loading, security, exception handling, and thread synchronization) are available to any and all programming languages that target it—period.

A managed module is a standard 32-bit Windows portable executable (PE32) file or a standard 64-bit Windows portable executable (PE32+) file that requires the CLR to execute. By the way, managed assemblies always take advantage of Data Execution Prevention (DEP) and Address Space Layout Randomization (ASLR) in Windows; these two features improve the security of your whole system.

Untitled

Untitled

Native code compilers produce code targeted to a specific CPU architecture, such as x86, x64, or ARM. All CLR-compliant compilers produce IL code instead. (I’ll go into more detail about IL code later in this chapter.) IL code is sometimes referred to as managed code because the CLR manages its execution.

💡 关于什么是 x86,x64 和 ARM 架构,简单来说就是前两个是 CISC(复杂指令集),后一个是 RISC(精简指令集),具体区别自行 Google。

In addition to emitting IL, every compiler targeting the CLR is required to emit full metadata into every managed module. In brief, metadata is a set of data tables that describe what is defined in the module, such as types and their members. In addition, metadata also has tables indicating what the managed module references, such as imported types and their members. Metadata is a superset of older technologies such as COM’s Type Libraries and Interface Definition Language (IDL) files. The important thing to note is that CLR metadata is far more complete. And, unlike Type Libraries and IDL, metadata is always associated with the file that contains the IL code. In fact, the metadata is always embedded in the same EXE/DLL as the code, making it impossible to separate the two. Because the compiler produces the metadata and the code at the same time and binds them into the resulting managed module, the metadata and the IL code it describes are never out of sync with one another.

Metadata removes the need for native C/C++ header and library files when compiling because
all the information about the referenced types/members is contained in the file that has the
IL that implements the type/members. Compilers can read metadata directly from managed
modules.

Microsoft’s C#, Visual Basic, F#, and the IL Assembler always produce modules that contain managed code (IL) and managed data (garbage-collected data types). End users must have the CLR (presently shipping as part of the .NET Framework) installed on their machine in order to execute any modules that contain managed code and/or managed data in the same way that they must have the Microsoft Foundation Class (MFC) library or Visual Basic DLLs installed to run MFC or Visual Basic 6.0 applications.

By default, Microsoft’s C++ compiler builds EXE/DLL modules that contain unmanaged (native)
code and manipulate unmanaged data (native memory) at run time.

The flexibility provided by Microsoft’s C++ compiler is unparalleled by other compilers because it allows developers to use their existing native C/C++ code from managed code and to start integrating the use of managed types as they see fit.

💡 小结:公共运行时(Common Language Runtime,CLR)是一个可由多种编程语言使用的” 运行时 “,CLR 的核心功能可以被所有面向 CLR 的语言使用。经过” 运行时 “的语言编译器的编译后,产生的是托管模块(managed module),托管模块又叫做 PE 文件,分为标准的 32 位 PE32 文件和标准的 64 位 PE32 + 文件,它们都需要 CLR 才能执行。每个托管模块又被分为上图 4 个部分。本机代码编译器生成的是面向特定 CPU 架构的代码,而面向 CLR 的编译器生成都是 IL 代码,IL 代码也被称作托管代码,由 CLR 管理它的执行。除了生成 IL,面向 CLR 的每个编译器还要在每个托管模块中生成完整的元数据(metadata)。元数据总是和包含 IL 代码的文件关联,它们被嵌入到了和代码相同的 EXE/DLL 文件当中(后面可以看到 DLL 文件由多个托管模块组成),使两者密不可分,正是因为在实现类型 / 成员的 IL 代码中已经包含了有关引用类型 / 成员的全部信息,所以编译器可以直接从托管代码中读取元数据,从而避免了编译时对原生 C/C++ 头和库文件的需求。为了执行包含托管代码的模块,用户机上必须安装 CLR(作为.NET Framework 的一部分提供)。

# Combining Managed Modules into Assemblies

The CLR doesn’t actually work with modules, it works with assemblies. An assembly is an abstract concept that can be difficult to grasp initially. First, an assembly is a logical grouping of one or more modules or resource files. Second, an assembly is the smallest unit of reuse, security, and versioning. Depending on the choices you make with your compilers or tools, you can produce a single-file or a multifile assembly. In the CLR world, an assembly is what we would call a component.

Figure 1-2 should help explain what assemblies are about. In this figure, some managed modules and resource (or data) files are being processed by a tool. This tool produces a single PE32(+) file that represents the logical grouping of files. What happens is that this PE32(+) file contains a block of data called the manifest. The manifest is simply another set of metadata tables. These tables describe the files that make up the assembly, the publicly exported types implemented by the files in the assembly, and the resource or data files that are associated with the assembly.

Untitled

An assembly’s modules also include information about referenced assemblies (including their
version numbers). This information makes an assembly self-describing. In other words, the CLR can determine the assembly’s immediate dependencies in order for code in the assembly to execute. No additional information is required in the registry or in Active Directory Domain Services (ADDS). Because no additional information is needed, deploying assemblies is much easier than deploying unmanaged components.

💡 小结:CLR 实际上不和模块工作,而是和程序集工作,在 CLR 的世界中,程序集相当于 “组件”。一些托管模块和资源(或数据)文件交由一个工具处理成一个 PE32 (+) 文件,就是前文提到的托管模块,只不过它包含了一个名为清单(manifest)的数据块,清单也是元数据表的集合,该托管模块经编译器转换为程序集。也就是说,程序集就是 C# 编译器生成的含有清单的托管模块,清单指出了程序集只有一个文件构成。程序集的模块中还包含了自描述(self-describing)的信息,从而不需要再注册表或 ADDS 中保存额外的信息,因此更容易部署。

# Loading the Common Language Runtime

Each assembly you build can be either an executable application or a DLL containing a set of types for use by an executable application.

Before we start looking at how the CLR loads, we need to spend a moment discussing 32-bit and 64-bit versions of Windows. If your assembly files contain only type-safe managed code, you are writing code that should work on both 32-bit and 64-bit versions of Windows. No source code changes are required for your code to run on either version of Windows. In fact, the resulting EXE/DLL file produced by the compiler should work correctly when running on x86 and x64 versions of Windows. In addition, Windows Store applications or class libraries will run on Windows RT machines (which use an ARM CPU). In other words, the one file will run on any machine that has the corresponding version of the .NET Framework installed on it.

💡 Note:关于什么是基于 ARM 的 WinRT 系统,总结起来就是,在针对 ARM 适配的 Windows RT 系统中,底层并没有使用.Net Framework,而是和 x86 架构的 Windows 一样,只是在 Kernel 层面对 ARM 指令进行了支持。在 Kernel 之上的不管是 Windows APIs、Windows Runtime APIs 还是.Net Framework,除了少部分实现可能需要做针对 ARM 指令的特殊处理,其它该是什么样就是什么样。至于更顶层的一般应用,在大部分时候甚至感知不到底层到底用了 x86 还是 ARM 架构的芯片。

On extremely rare occasions, developers want to write code that works only on a specific version of Windows. Developers might do this when using unsafe code or when interoperating with unmanaged code that is targeted to a specific CPU architecture. To aid these developers, the C# compiler offers a /platform command-line switch. This switch allows you to specify whether the resulting assembly can run on x86 machines running 32-bit Windows versions only, x64 machines running 64-bit Windows only, or ARM machines running 32-bit Windows RT only. If you don’t specify a platform, the default is anycpuanycpu, which indicates that the resulting assembly can run on any version of Windows.

Untitled

Depending on the platform switch, the C# compiler will emit an assembly that contains either a PE32 or PE32+ header, and the compiler will also emit the desired CPU architecture (or agnostic) into the header as well. Microsoft ships two SDK command-line utilities, DumpBin.exe and CorFlags.exe, that you can use to examine the header information emitted in a managed module by the compiler.

When running an executable file, Windows examines this EXE file’s header to determine whether the application requires a 32-bit or 64-bit address space. A file with a PE32 header can run with a 32-bit or 64-bit address space, and a file with a PE32+ header requires a 64-bit address space. Windows also checks the CPU architecture information embedded inside the header to ensure that it matches the CPU type in the computer. Lastly, 64-bit versions of Windows offer a technology that allows 32-bit Windows applications to run. This technology is called WoW64 (for Windows on Windows 64).

Untitled

After Windows has examined the EXE file’s header to determine whether to create a 32-bit or 64-bit process, Windows loads the x86, x64, or ARM version of MSCorEE.dll into the process’s address space. Then, the process’s primary thread calls a method defined inside MSCorEE.dll. This method initializes the CLR, loads the EXE assembly, and then calls its entry point method (Main). At this point, the managed application is up and running.

If an unmanaged application calls the Win32 LoadLibraryLoadLibrary function to load a managed assembly, Windows knows to load and initialize the CLR (if not already loaded) in order to process the code contained within the assembly. Of course, in this scenario, the process is already up and running, and this may limit the usability of the assembly. For example, a managed assembly compiled with the /platform:x86 switch will not be able to load into a 64-bit process at all, whereas an executable file compiled with this same switch would have loaded in WoW64 on a computer running a 64-bit version of Windows.

💡 小结:对于类型安全的托管代码来说,代码在 32 位和 64 位 Windows 上都能正常工作,代码无需任何改动,而对于使用 ARM CPU 的 WinRT 机器来说,只要安装了对应版本的.NET Framework,文件就能正常运行。有时需要使用不安全的代码或者要和特定架构的非托管代码进行互操作时,可以使用 /platform 命令行开关选项。在运行一个应用程序时,Windows 检查 EXE 文件头,决定是创建 32 位还是 64 位进程后,会在进程地址空间加载 MSCorEE.dll 的 x86、x64 或 ARM 版本。然后进程的主线程调用 MSCorEE.dll 中定义的一个方法。这个方法初始化 CLR,加载 EXE 程序集,再调用其入口方法(Main)。随即,托管应用程序启动并运行。

# Executing Your Assembly’s Code

As mentioned earlier, managed assemblies contain both metadata and IL. IL is a CPU-independent machine language created by Microsoft after consultation with several external commercial and academic language/compiler writers. IL is a much higher-level language than most CPU machine languages. IL can access and manipulate object types and has instructions to create and initialize objects, call virtual methods on objects, and manipulate array elements directly. It even has instructions to throw and catch exceptions for error handling. You can think of IL as an object-oriented machine language.

Figure 1-4 shows what happens the first time a method is called.

Untitled

Just before the Main method executes, the CLR detects all of the types that are referenced by
Main’s code. This causes the CLR to allocate an internal data structure that is used to manage access to the referenced types. In Figure 1-4, the Main method refers to a single type, Console, causing the CLR to allocate a single internal structure. This internal data structure contains an entry for each method defined by the Console type. Each entry holds the address where the method’s implementation can be found. When initializing this structure, the CLR sets each entry to an internal, undocumented function contained inside the CLR itself. I call this function JITCompiler.

When Main makes its first call to WriteLine, the JITCompiler function is called. The JITCompiler function is responsible for compiling a method’s IL code into native CPU instructions.
Because the IL is being compiled “just in time,” this component of the CLR is frequently referred to as a JITter or a JIT compiler.

When called, the JITCompiler function knows what method is being called and what type defines this method. The JITCompiler function then searches the defining assembly’s metadata for the called method’s IL. JITCompiler next verifies and compiles the IL code into native CPU instructions. The native CPU instructions are saved in a dynamically allocated block of memory. Then, JITCompiler goes back to the entry for the called method in the type’s internal data structure created by the CLR and replaces the reference that called it in the first place with the address of the block of memory containing the native CPU instructions it just compiled. Finally, the JITCompiler function jumps to the code in the memory block. This code is the implementation of the WriteLine method (the version that takes a String parameter). When this code returns, it returns to the code in Main, which continues execution as normal.

Untitled

Main now calls WriteLine a second time. This time, the code for WriteLine has already been
verified and compiled. So the call goes directly to the block of memory, skipping the JITCompiler function entirely. After the WriteLine method executes, it returns to Main. Figure 1-5 shows what the process looks like when WriteLine is called the second time.

A performance hit is incurred only the first time a method is called. All subsequent calls to the
method execute at the full speed of the native code because verification and compilation to native code don’t need to be performed again.

You should also be aware that the CLR’s JIT compiler optimizes the native code just as the back end of an unmanaged C++ compiler does. Again, it may take more time to produce the optimized code, but the code will execute with much better performance than if it hadn’t been optimized.

Untitled

For those developers coming from an unmanaged C or C++ background, you’re probably thinking about the performance ramifications of all this. After all, unmanaged code is compiled for a specific CPU platform, and, when invoked, the code can simply execute. In this managed environment, compiling the code is accomplished in two phases. First, the compiler passes over the source code, doing as much work as possible in producing IL. But to execute the code, the IL itself must be compiled into native CPU instructions at run time, requiring more non-shareable memory to be allocated and requiring additional CPU time to do the work.

# IL and Verification

IL is stack-based, which means that all of its instructions push operands onto an execution stack and pop results off the stack. Because IL offers no instructions to manipulate registers, it is easy for people to create new languages and compilers that produce code targeting the CLR.

IL instructions are also typeless. For example, IL offers an add instruction that adds the last two
operands pushed on the stack. There are no separate 32-bit and 64-bit versions of the add instruction. When the add instruction executes, it determines the types of the operands on the stack and performs the appropriate operation.

While compiling IL into native CPU instructions, the CLR performs a process called verification. Verification examines the high-level IL code and ensures that everything the code does is safe.

In Windows, each process has its own virtual address space. Separate address spaces are necessary because you can’t trust an application’s code. It is entirely possible (and unfortunately, all too common) that an application will read from or write to an invalid memory address. By placing each Windows process in a separate address space, you gain robustness and stability; one process can’t adversely affect another process.

By verifying the managed code, however, you know that the code doesn’t improperly access
memory and can’t adversely affect another application’s code. This means that you can run multiple managed applications in a single Windows virtual address space.

Because Windows processes require a lot of operating system resources, having many of them
can hurt performance and limit available resources. Reducing the number of processes by running multiple applications in a single operating system process can improve performance, require fewer resources, and be just as robust as if each application had its own process. This is another benefit of managed code as compared to unmanaged code.

# Unsafe Code

By default, Microsoft’s C# compiler produces safe code. Safe code is code that is verifiably safe. However, Microsoft’s C# compiler allows developers to write unsafe code. Unsafe code is allowed to work directly with memory addresses and can manipulate bytes at these addresses. This is a very powerful feature and is typically useful when interoperating with unmanaged code or when you want to improve the performance of a time-critical algorithm.

However, using unsafe code introduces a significant risk: unsafe code can corrupt data structures and exploit or even open up security vulnerabilities. For this reason, the C# compiler requires that all methods that contain unsafe code be marked with the unsafe keyword. In addition, the C# compiler requires you to compile the source code by using the /unsafe compiler switch.

💡 小结:IL 是与 CPU 无关的机器语言,IL 能访问和操作对象类型,而且提供了指令来创建和初始化对象、调用对象上的虚方法以及直接操作数组元素,甚至提供了抛出和捕捉异常的指令来实现错误处理。IL 也能使用汇编语言编写,高级语言通常只公开了 CLR 全部功能的一个子集,但通过 IL 汇编语言我们可以使用 CLR 的全部功能。允许在不同编程语言之间方便地切换,同时有保持紧密集成,这是 CLR 的一个很出众的特点。执行方法时,CLR 的 JIT 编译器负责把方法的 IL 代码转换成本机 CPU 指令,JIT 是 just in time 的意思,只有在第一次使用到程序集中的方法时,CLR 会分配该方法引用类型的一个内部结构,在这个数据结构中,类型中的每一个方法都有一个对应的记录项,初始时会被设置成指向 JIPCompiler,之后 JITCompiler 在定义该类型的程序集的元数据中查找被调用方法的 IL,此时会有一个验证的过程,验证过程会检查 IL 代码,确定代码所作的一切都是安全的。之后 IL 代码被编译成本机 CPU 指令保存在动态分配的内存块中(这就解释了为什么应用程序一旦终止,第二次运行时仍需编译的原因)。然后 JITCompiler 会回到 CLR 为类型创建的内部数据结构,找到与被调用方法对应的那条记录,修改最初对 JITCompiler 的引用,使其指向内存块中编译好的本机 CPU 指令的地址。最后,JITCompiler 执行内存块中的代码,执行完毕返回到 Main 中的下一条语句继续执行,若第二次执行相同方法时,则会直接执行内存块中的代码。可以看出,方法只有在首次调用时才存在性能损失。通过对 Linux 中软件包管理知识的学习,我们知道 C/C++ 的源码包针对一种具体 CPU 平台进行编译,从而生成适合于本机操作系统的二进制文件,源码包编译安装完成后,一旦调用,代码直接就能运行。而在托管环境中,代码的编译是分两个阶段完成的。首先,编译器遍历源代码,做大量的工作来生成 IL 代码。但要真正执行,这些 IL 代码本身必须在运行时编译成本机 CPU 指令,这就需要分配更多的非共享内存,并且要花费额外的 CPU 时间。不过,JIT 编译也有它的优点,例如它能使用提升性能的特殊指令、生成的本机代码将针对主机进行优化,使最终代码变得更小,执行得更快,最大的优势是,它保证了应用程序的健壮性和安全性,它能验证代码的安全性,能用一个进程运行多个应用程序并且保证代码不会不正确地访问内存,不会干扰到另一个应用程序的代码。可以看到,在这些方面托管应用程序的性能实际上超越了非托管应用程序。

# The Native Code Generator Tool: NGen.exe

The NGen.exe tool that ships with the .NET Framework can be used to compile IL code to native code when an application is installed on a user’s machine. Because the code is compiled at install time, the CLR’s JIT compiler does not have to compile the IL code at run time, and this can improve the application’s performance. The NGen.exe tool is interesting in two scenarios:

Improving an application’s startup time

Running NGen.exe can improve startup time because the code will already be compiled into native code so that compilation doesn’t have to occur at run time.

Reducing an application’s working set

If you believe that an assembly will be loaded into multiple processes simultaneously, running NGen.exe on that assembly can reduce the applications’ working set. The reason is because the NGen.exe tool compiles the IL to native code and saves the output in a separate file. This file can be memory-mapped into multiple-process address spaces simultaneously, allowing the code to be shared; not every process needs its own copy of the code.

💡 Note:所谓工作集(working set),是指在进程的所有内存中,已映射的物理内存那一部分(即这些内存块全在物理内存中,并且 CPU 可以直接访问);进程还有一部分虚拟内存,它们可能在转换列表中(CPU 不能通过虚地址访问,需要 Windows 映射之后才能访问);还有一部分内存在磁盘上的分页文件里。

Now, whenever the CLR loads an assembly file, the CLR looks to see if a corresponding NGen’d native file exists. If a native file cannot be found, the CLR JIT compiles the IL code as usual. However, if a corresponding native file does exist, the CLR will use the compiled code contained in the native file, and the file’s methods will not have to be compiled at run time.

On the surface, this sounds great! It sounds as if you get all of the benefits of managed code (garbage collection, verification, type safety, and so on) without all of the performance problems of managed code (JIT compilation). However, the reality of the situation is not as rosy as it would first seem. There are several potential problems with respect to NGen’d files:

No intellectual property protection

Many people believe that it might be possible to ship NGen’d files without shipping the files containing the original IL code, thereby keeping their intellectual property a secret. Unfortunately, this is not possible. At run time, the CLR requires access to the assembly’s metadata (for functions such as reflection and serialization); this requires that the assemblies that contain IL and metadata be shipped. In addition, if the CLR can’t use the NGen’d file for some reason (described next), the CLR gracefully goes back to JIT compiling the assembly’s IL code, which must be available.

NGen’d files can get out of sync

When the CLR loads an NGen’d file, it compares a number of characteristics about the previously compiled code and the current execution environment. If any of the characteristics don’t match, the NGen’d file cannot be used, and the normal JIT compiler process is used instead. Here is a partial list of characteristics that must match:

  • CLR version: This changes with patches or service packs.
  • CPU type: This changes if you upgrade your processor hardware.
  • Windows operating system version: This changes with a new service pack update.
  • Assembly’s identity module version ID (MVID): This changes when recompiling.
  • Referenced assembly’s version IDs: This changes when you recompile a referenced assembly.
  • Security: This changes when you revoke permissions (such as declarative inheritance, declarative link-time), SkipVerification , or UnmanagedCode permissions), that were once granted.

Inferior execution-time performance

When compiling code, NGen can’t make as many assumptions about the execution environment as the JIT compiler can. This causes NGen.exe to produce inferior code. Some NGen’d applications actually perform about 5 percent slower when compared to their JIT-compiled counterpart. So, if you’re considering using NGen.exe to improve the performance of your application, you should compare NGen’d and non-NGen’d versions to be sure that the NGen’d version doesn’t actually run slower! For some applications, the reduction in working set size improves performance, so using NGen can be a net win.

For large client applications that experience very long startup times, Microsoft provides a Managed Profile Guided Optimization tool (MPGO.exe). This tool analyzes the execution of your application to see what it needs at startup. This information is then fed to the NGen.exe tool in order to better optimize the resulting native image. This allows your application to start faster and with a reduced working set.

💡 小结:使用.NET Framework 提供的 NGen.exe 工具,可以在应用程序安装到用户的计算机上时,将 IL 代码编译成本机代码。这种方式能够提高应用程序的启动速度,并且减少应用程序的工作集(Gen.exe 将 IL 代码编译成本机代码,并将这些代码保存在单独的文件中。该文件可以通过 “内存映射” 的方式,同时映射到多个进程地址空间中,使代码得到了共用,避免每个进程都需要一份单独的代码拷贝)。但是,使用 NGen 也会带来一些问题,例如:没有知识产权保护,即使发布 NGen 生成的文件而不发布包含原始 IL 代码的文件还是不能保护知识产权,因为在运行时,CLR 要求访问程序集的元数据(用于反射和序列化等功能),这就要求发布包含 IL 和元数据的程序集;NGen 生成的文件可能失去同步,CLR 加载 NGen 生成的文件时,会将预编译代码的许多特征与当前执行环境进行比较,任何特称不匹配,NGen 生成的文件就不能使用;较差的执行性能,编译代码时,NGen 无法想 JIT 编译器那样对执行环境进行许多假定,例如不能优化 CPU 指令、静态字段只能间接访问,而不能直接访问,因为静态字段的实际地址只能在运行时确定,也不能知道一个类构造器是否已经调用等问题。因此使用前应仔细比较 NGen 版本和非 NGen 版本,谨慎使用。对于启动很慢的大型客户端应用程序,可以考虑使用 MPGO.exe(Managed Profile Guided Optimization),该工具分析程序执行,检查它在启动时需要的东西,并把这些信息返回给 NGen.exe 来更好地优化本机映像,这使应用程序启动得更快,工作集也缩小了。

# The Framework Class Library

The .NET Framework includes the Framework Class Library (FCL). The FCL is a set of DLL assemblies that contain several thousand type definitions in which each type exposes some functionality. Microsoft is producing additional libraries such as the Windows Azure SDK and the DirectX SDK. These additional libraries provide even more types, exposing even more functionality for your use. In fact, Microsoft is producing many libraries at a phenomenal rate, making it easier than ever for developers to use various Microsoft technologies.

Here are just some of the kinds of applications developers can create by using these assemblies:

Web services

Web Forms/MVC HTML-based applications (websites)

Rich Windows GUI applications

Windows console applications

Windows services

Database stored procedures

Component library

Most of the namespaces in the FCL present types that can be used for any kind of application. Table 1-3 lists some of the more general namespaces and briefly describes what the types in that namespace are used for. This is a very small sampling of the namespaces available. Please see the documentation that accompanies the various Microsoft SDKs to gain familiarity with the ever-growing set of namespaces that Microsoft is producing.

Untitled

💡 小结:.NET Framework 包含了 Framework 类库(Framework Class Library,FCL)。FCL 是一组 DLL 程序集的统称,其中包含了数千个类型定义,每个类型都公开了一些功能,通过使用这些程序集我们能创建一些基于.NET Framework 的应用程序。

# The Common Type System

By now, it should be obvious to you that the CLR is all about types. Types expose functionality to your applications and other types. Types are the mechanism by which code written in one programming language can talk to code written in a different programming language. Because types are at the root of the CLR, Microsoft created a formal specification—the Common Type System (CTS)—that describes how types are defined and how they behave.

💡 Note : Microsoft 事实上已将 CTS 和.NET Framework 的其他组件 — 包括文件格式、元数据、中间语言以及对底层平台的访问(P/Invoke)— 提交给 ECMA 以完成标准化工作。最后形成的标准称为 “公共语言基础结构”(Common Language Infrastructure, CLI)。除此之外,Microsoft 还提交了 Framework 类库的一部分、C# 编程语言(ECMA-334)以及 C++/CLI 编程语言。

The CTS specification states that a type can contain zero or more members. In Part II, “Designing Types,” I’ll cover all of these members in great detail. For now, I just want to give you a brief introduction to them:

Field

A data variable that is part of the object’s state. Fields are identified by their name and type.

Method

A function that performs an operation on the object, often changing the object’s state. Methods have a name, a signature, and modifiers. The signature specifies the number of parameters (and their sequence), the types of the parameters, whether a value is returned by the method, and if so, the type of the value returned by the method.

Property

To the caller, this member looks like a field. But to the type implementer, it looks like a method (or two). Properties allow an implementer to validate input parameters and object state before accessing the value and/or calculating a value only when necessary. They also allow a user of the type to have simplified syntax. Finally, properties allow you to create read-only or write-only "fields".

Event

An event allows a notification mechanism between an object and other interested objects. For example, a button could offer an event that notifies other objects when the button is clicked.

The CTS also specifies the rules for type visibility and access to the members of a type. Thus, the CTS establishes the rules by which assemblies form a boundary of visibility for a type, and the CLR enforces the visibility rules.

A type that is visible to a caller can further restrict the ability of the caller to access the type’s members. The following list shows the valid options for controlling access to a member:

Private

The member is accessible only by other members in the same class type.

Family

The member is accessible by derived types, regardless of whether they are within the same assembly. Note that many languages (such as C++ and C#) refer to family as protected.

Family and assembly

The member is accessible by derived types, but only if the derived type is defined in the same assembly. Many languages (such as C# and Visual Basic) don’t offer this access control. Of course, IL Assembly language makes it available.

Assembly

The member is accessible by any code in the same assembly. Many languages refer to assembly as internal.

Family or assembly

The member is accessible by derived types in any assembly. The member is also accessible by any types in the same assembly. C# refers to family or assembly as protected internal.

Public

The member is accessible by any code in any assembly.

In addition, the CTS defines the rules governing type inheritance, virtual methods, object lifetime, and so on. These rules have been designed to accommodate the semantics expressible in modern day programming languages. In fact, you won’t even need to learn the CTS rules per se because the language you choose will expose its own language syntax and type rules in the same way that you’re familiar with today. And it will map the language-specific syntax into IL, the “language” of the CLR, when it emits the assembly during compilation.

Sure, the syntax you use for defining the type is different depending on the language you choose, but the behavior of the type will be identical regardless of the language because the CLR’s CTS defines the behavior of the type.

Here’s another CTS rule. All types must (ultimately) inherit from a predefined type: System.Object. As you can see, Object is the name of a type defined in the System namespace. This Object is the root of all other types and therefore guarantees that every type instance has a minimum set of behaviors. Specifically, the System.Object type allows you to do the following:

■ Compare two instances for equality.

■ Obtain a hash code for the instance.

■ Query the true type of an instance.

■ Perform a shallow (bitwise) copy of the instance.

■ Obtain a string representation of the instance object’s current state.

💡 小结:通过类型,用一种编程语言写的代码能与用另一种编程语言写的代码沟通。由于类型是 CLR 的根本,所以 Microsoft 制定了一个正式的规范来描述类型的定义和行为,也就是 “通用类型系统”(Common Type System, CTS)。而 CTS 和.NET Framework 的其他组件(包括文件格式、元数据、中间语言以及对底层平台的访问 P/Invoke)提交给 ECMA 形成的标准又称为 “公用语言基础结构”(Common Language Infrastructure, CLI)。CTS 规范规定了一个类型可以包含零个或者多个成员,这些成员包括:字段、方法、属性、事件。CTS 还指定了类型可见性规则以及类型成员的访问规则,利用 CTS 制定的规则,程序集为一个类型建立了可视边界,CLR 则强制(贯彻)了这些规则。除此之外,CTS 还为类型继承、虚方法、对象生存期等定义了相应的规则。通过编译来生成程序集时,它会将语言特有的语法映射到 IL— 也就是 CLR 的 “语言”。无论使用哪一种语言,类型的行为都完全一致,因为最终是由 CLR 的 CTS 来定义类型的行为。另一条 CTS 规则是:所有类型最终必须从预定义的 System.Object 类型继承。Object 是其他所有类型的根,因而保证了每个类型实例都有一组最基本的行为。

# The Common Language Specification

COM allows objects created in different languages to communicate with one another. On the other hand, the CLR now integrates all languages and allows objects created in one language to be treated as equal citizens by code written in a completely different language. This integration is possible because of the CLR’s standard set of types, metadata (self-describing type information), and common execution environment.

If you intend to create types that are easily accessible from other programming languages, you need to use only features of your programming language that are guaranteed to be available in all other languages. To help you with this, Microsoft has defined a Common Language Specification (CLS) that details for compiler vendors the minimum set of features their compilers must support if these compilers are to generate types compatible with other components written by other CLS-compliant languages on top of the CLR.

The CLR/CTS supports a lot more features than the subset defined by the CLS, so if you don’t care about interlanguage operability, you can develop very rich types limited only by the language’s feature set. Specifically, the CLS defines rules that externally visible types and methods must adhere to if they are to be accessible from any CLS-compliant programming language. Note that the CLS rules don’t apply to code that is accessible only within the defining assembly. Figure 1-6 summarizes the ideas expressed in this paragraph.

Untitled

As Figure 1-6 shows, the CLR/CTS offers a set of features. Some languages expose a large subset of the CLR/CTS. A programmer willing to write in IL assembly language, for example, is able to use all of the features the CLR/CTS offers. Most other languages, such as C#, Visual Basic, and Fortran, expose a subset of the CLR/CTS features to the programmer. The CLS defines the minimum set of features that all languages must support.

If you’re designing a type in one language, and you expect that type to be used by another language, you shouldn’t take advantage of any features that are outside of the CLS in its public and protected members. Doing so would mean that your type’s members might not be accessible by programmers writing code in other programming languages.

Let me distill the CLS rules to something very simple. In the CLR, every member of a type is either a field (data) or a method (behavior). This means that every programming language must be able to access fields and call methods. Certain fields and certain methods are used in special and common ways. To ease programming, languages typically offer additional abstractions to make coding these common programming patterns easier. For example, languages expose concepts such as enums, arrays, properties, indexers, delegates, events, constructors, finalizers, operator overloads, conversion operators, and so on. When a compiler comes across any of these things in your source code, it must translate these constructs into fields and methods so that the CLR and any other programming language can access the construct.

using System;
internal sealed class Test {
 // Constructor
 public Test() {}
 // Finalizer
 ~Test() {}
 // Operator overload
 public static Boolean operator == (Test t1, Test t2) {
 return true;
 }
 public static Boolean operator != (Test t1, Test t2) {
 return false;
 }
 // An operator overload
 public static Test operator + (Test t1, Test t2) { return null; }
 // A property
 public String AProperty {
    get { return null; }
    set { }
 }
 // An indexer
 public String this[Int32 x] {
    get { return null; }
    set { }
 }
 // An event
 public event EventHandler AnEvent;
}

Untitled

When the compiler compiles this code, the result is a type that has a number of fields and methods defined in it. You can easily see this by using the IL Disassembler tool (ILDasm.exe) provided with the .NET Framework SDK to examine the resulting managed module, which is shown in Figure 1-7.

Untitled

FIGURE 1-7 ILDasm showing Test type’s fields and methods (obtained from metadata)

Untitled

Untitled

The additional nodes under the Test type that aren’t mentioned in Table 1-4—.class, .custom, AnEvent, AProperty, and Item—identify additional metadata about the type. These nodes don’t map to fields or methods; they just offer some additional information about the type that the CLR, programming languages, or tools can get access to.

💡 小结:不同语言创建的对象可通过 COM(Component Object Model)通信。CLR 则集成了所有的语言,用一种语言创建对象在另一种语言中,和用后者创建的对象具有相同的地位。之所以能实现这样的集成,是因为 CLR 使用了标准类型集、元数据(自描述的类型信息)以及公共执行环境。为了很容易从其他编程语言中访问类型,Microsoft 定义了 “公共语言规范”(Common Language Specification, CLS),它详细定义了一个最小功能集。任何编译器只有支持这个功能集,生成的类型才能兼容由其他符合 CLS、面向 CLR 的语言生成的组件。CLR/CTS 支持的类型比 CLS 定义的多得多,CLS 定义的只是一个子集。在开发类型和方法时,如果希望它们对外 “可见”,能从符合 CLS 的任何编程语言中访问,就必须遵守 CLS 定义的规则。如果开发人员用 IL 汇编语言些程序,可以使用 CLR/CTS 提供的全部功能。用一种语言定义类型时,如果希望在另一种语言中使用该类型,就不要在该类型的 public 和 protected 成员中使用位于 CLS 外部的任何功能。否则,其他开发人员使用其他语言写代码时,就可能无法访问这个类型的成员。现在提炼一下 CLS 的规则。在 CLR 中,类型的每个成员要么时字段(数据),要么是方法(行为)。为简化编程,语言往往提供了额外的抽象,从而对这些常见的编程模式进行简化。例如,语言会公开枚举、数组、属性、索引器、委托、事件、构造器、终结器、操作符重载、转换操作符等概念。编译器在源代码中遇到其中任何一样,都必须将其转换成字段和方法,使 CLR 和其他任何编程语言能够访问这些构造。

# Interoperability with Unmanaged Code

The .NET Framework offers a ton of advantages over other development platforms. However, very few companies can afford to redesign and re-implement all of their existing code. Microsoft realizes this and has constructed the CLR so that it offers mechanisms that allow an application to consist of both managed and unmanaged parts. Specifically, the CLR supports three interoperability scenarios:

Managed code can call an unmanaged function in a DLL

Managed code can use an existing COM component (server)

Unmanaged code can use a managed type (server)

With Windows 8, Microsoft has introduced a new Windows API called the Windows Runtime (WinRT). This API is implemented internally via COM components. But, instead of using type library files, the COM components describe their API via the metadata ECMA standard created by the .NET Framework team. The beauty of this is that code written via a .NET language can (for the most part) seamlessly communicate with WinRT APIs. Underneath the covers, the CLR is performing all of the COM interop for you without you having to use any additional tools at all—it just works! Chapter 25, “Interoperating with WinRT Components” goes into all the details.

💡 小结:Microsoft 通过 CLR 提供了一些机制,允许在应用程序中同时包含托管和非托管代码。CLR 支持三种互操作情形:托管代码能调用 DLL 中的非托管代码;托管代码可以使用现有 COM 组件(服务器);非托管代码可以使用托管类型(服务器)。